package com.reactnativestudy.utils;

import android.os.Build;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.rnziparchive.StreamUtil;

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.Zip4jConstants;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

interface Promise{
    void resolve(Object b);
    void reject(Error error, String message);
}
public class ZipUtils {

    private static final String TAG = com.rnziparchive.RNZipArchiveModule.class.getSimpleName();



    public static void isPasswordProtected(final String zipFilePath, final Promise promise) {
        try {
            net.lingala.zip4j.core.ZipFile zipFile = new net.lingala.zip4j.core.ZipFile(zipFilePath);
            promise.resolve(zipFile.isEncrypted());
        } catch (ZipException ex) {
            promise.reject(null, String.format("Unable to check for encryption due to: %s", getStackTrace(ex)));
        }
    }
//
    public static void unzipWithPassword(final String zipFilePath, final String destDirectory,
                                  final String password, final Promise promise) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    net.lingala.zip4j.core.ZipFile zipFile = new net.lingala.zip4j.core.ZipFile(zipFilePath);
                    if (zipFile.isEncrypted()) {
                        zipFile.setPassword(password);
                    } else {
                        promise.reject(null, String.format("ZipUtils file: %s is not password protected", zipFilePath));
                    }

                    List fileHeaderList = zipFile.getFileHeaders();
                    List extractedFileNames = new ArrayList<>();
                    int totalFiles = fileHeaderList.size();

                    updateProgress(0, 1, zipFilePath); // force 0%
                    for (int i = 0; i < totalFiles; i++) {
                        FileHeader fileHeader = (FileHeader) fileHeaderList.get(i);

                        File fout = new File(destDirectory, fileHeader.getFileName());
                        String canonicalPath = fout.getCanonicalPath();
                        String destDirCanonicalPath = (new File(destDirectory).getCanonicalPath()) + File.separator;

                        if (!canonicalPath.startsWith(destDirCanonicalPath)) {
                            throw new SecurityException(String.format("Found ZipUtils Path Traversal Vulnerability with %s", canonicalPath));
                        }

                        zipFile.extractFile(fileHeader, destDirectory);
                        if (!fileHeader.isDirectory()) {
                            extractedFileNames.add(fileHeader.getFileName());
                        }
                        updateProgress(i + 1, totalFiles, zipFilePath);
                    }
                    promise.resolve(Arguments.fromList(extractedFileNames));
                } catch (Exception ex) {
                    updateProgress(0, 1, zipFilePath); // force 0%
                    promise.reject(null, String.format("Failed to unzip file, due to: %s", getStackTrace(ex)));
                }
            }
        }).start();
    }

    public static void unzip(final String zipFilePath, final String destDirectory, final String charset, final Promise promise) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // Check the file exists
                FileInputStream inputStream = null;
                try {
                    inputStream = new FileInputStream(zipFilePath);
                    new File(zipFilePath);
                } catch (FileNotFoundException | NullPointerException e) {
                    if (inputStream != null) {
                        try {
                            inputStream.close();
                        } catch (IOException ignored) {
                        }
                    }
                    promise.reject(null, "Couldn't open file " + zipFilePath + ". ");
                    return;
                }

                try {
                    // Find the total uncompressed size of every file in the zip, so we can
                    // get an accurate progress measurement
                    final long totalUncompressedBytes = getUncompressedSize(zipFilePath, charset);

                    File destDir = new File(destDirectory);
                    if (!destDir.exists()) {
                        //noinspection ResultOfMethodCallIgnored
                        destDir.mkdirs();
                    }

                    updateProgress(0, 1, zipFilePath); // force 0%

                    // We use arrays here so we can update values
                    // from inside the callback
                    final long[] extractedBytes = {0};
                    final int[] lastPercentage = {0};

                    ZipFile zipFile = null;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        zipFile = new ZipFile(zipFilePath, Charset.forName(charset));
                    } else {
                        zipFile = new ZipFile(zipFilePath);
                    }

                    final Enumeration<? extends ZipEntry> entries = zipFile.entries();
                    Log.d(TAG, "ZipUtils has " + zipFile.size() + " entries");
                    while (entries.hasMoreElements()) {
                        final ZipEntry entry = entries.nextElement();
                        if (entry.isDirectory()) continue;

                        StreamUtil.ProgressCallback cb = new StreamUtil.ProgressCallback() {
                            @Override
                            public void onCopyProgress(long bytesRead) {
                                extractedBytes[0] += bytesRead;

                                int lastTime = lastPercentage[0];
                                int percentDone = (int) ((double) extractedBytes[0] * 100 / (double) totalUncompressedBytes);

                                // update at most once per percent.
                                if (percentDone > lastTime) {
                                    lastPercentage[0] = percentDone;
                                    updateProgress(extractedBytes[0], totalUncompressedBytes, zipFilePath);
                                }
                            }
                        };

                        File fout = new File(destDirectory, entry.getName());
                        String canonicalPath = fout.getCanonicalPath();
                        String destDirCanonicalPath = (new File(destDirectory).getCanonicalPath()) + File.separator;

                        if (!canonicalPath.startsWith(destDirCanonicalPath)) {
                            throw new SecurityException(String.format("Found ZipUtils Path Traversal Vulnerability with %s", canonicalPath));
                        }

                        if (!fout.exists()) {
                            //noinspection ResultOfMethodCallIgnored
                            (new File(fout.getParent())).mkdirs();
                        }
                        InputStream in = null;
                        BufferedOutputStream Bout = null;
                        try {
                            in = zipFile.getInputStream(entry);
                            Bout = new BufferedOutputStream(new FileOutputStream(fout));
                            StreamUtil.copy(in, Bout, cb);
                            Bout.close();
                            in.close();
                        } catch (IOException ex) {
                            if (in != null) {
                                try {
                                    in.close();
                                } catch (Exception ignored) {
                                }
                            }
                            if (Bout != null) {
                                try {
                                    Bout.close();
                                } catch (Exception ignored) {
                                }
                            }
                        }
                    }

                    zipFile.close();
                    updateProgress(1, 1, zipFilePath); // force 100%
                    promise.resolve(destDirectory);
                } catch (Exception ex) {
                    updateProgress(0, 1, zipFilePath); // force 0%
                    promise.reject(null, "Failed to extract file " + ex.getLocalizedMessage());
                }
            }
        }).start();
    }


    public static void zip(String fileOrDirectory, String destDirectory, Promise promise) {
        try{

            ZipParameters parameters = new ZipParameters();
            parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
            parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);

            processZip(fileOrDirectory, destDirectory, parameters, promise);

        } catch (Exception ex) {
            promise.reject(null, ex.getMessage());
            return;
        }
    }


    public static void zipWithPassword(String fileOrDirectory, String destDirectory, String password,
                                String encryptionMethod, Promise promise) {
        try{

            ZipParameters parameters = new ZipParameters();
            parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
            parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);

            String encParts[] = encryptionMethod.split("-");

            if (password != null && !password.isEmpty()) {
                parameters.setEncryptFiles(true);
                if (encParts[0].equals("AES")) {
                    parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES);
                    if (encParts[1].equals("128")) {
                        parameters.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_128);
                    } else if (encParts[1].equals("256")) {
                        parameters.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_256);
                    } else {
                        parameters.setAesKeyStrength(Zip4jConstants.ENC_METHOD_STANDARD);
                    }
                } else if (encryptionMethod.equals("STANDARD")) {
                    parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD);
                    Log.d(TAG, "Standard Encryption");
                } else {
                    parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD);
                    Log.d(TAG, "Encryption type not supported default to Standard Encryption");
                }
                parameters.setPassword(password);
            } else {
                promise.reject(null, "Password is empty");
            }

            processZip(fileOrDirectory, destDirectory, parameters, promise);

        } catch (Exception ex) {
            promise.reject(null, ex.getMessage());
            return;
        }

    }

    private static void processZip(final String fileOrDirectory, final String destDirectory, final ZipParameters parameters, final Promise promise) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    net.lingala.zip4j.core.ZipFile zipFile = new net.lingala.zip4j.core.ZipFile(destDirectory);

                    updateProgress(0, 100, destDirectory);

                    File f = new File(fileOrDirectory);

                    int totalFiles = 0;
                    int fileCounter = 0;

                    if (f.exists()) {
                        if (f.isDirectory()) {

                            List<File> files = Arrays.asList(f.listFiles());

                            totalFiles = files.size();
                            for (int i = 0; i < files.size(); i++) {
                                if (files.get(i).isDirectory()) {
                                    zipFile.addFolder(files.get(i).getAbsolutePath(), parameters);
                                }
                                else {
                                    zipFile.addFile(files.get(i), parameters);
                                }
                                fileCounter += 1;
                                updateProgress(fileCounter, totalFiles, destDirectory);
                            }

                        } else {
                            totalFiles = 1;
                            zipFile.addFile(f, parameters);
                            fileCounter += 1;
                            updateProgress(fileCounter, totalFiles, destDirectory);
                        }
                    }
                    else {
                        promise.reject(null, "File or folder does not exist");
                    }

                    updateProgress(1, 1, destDirectory); // force 100%
                    promise.resolve(destDirectory);

                } catch (Exception ex) {
                    promise.reject(null, ex.getMessage());
                    return;
                }
            }
        }).start();
    }
    protected static void updateProgress(long extractedBytes, long totalSize, String zipFilePath) {

    }

    /**
     * Return the uncompressed size of the ZipFile (only works for files on disk, not in assets)
     *
     * @return -1 on failure
     */
    private static long getUncompressedSize(String zipFilePath, String charset) {
        long totalSize = 0;
        try {
            ZipFile zipFile = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                zipFile = new ZipFile(zipFilePath, Charset.forName(charset));
            } else {
                zipFile = new ZipFile(zipFilePath);
            }
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                long size = entry.getSize();
                if (size != -1) {
                    totalSize += size;
                }
            }
            zipFile.close();
        } catch (IOException ignored) {
            return -1;
        }
        return totalSize;
    }

    /**
     * Returns the exception stack trace as a string
     */
    private static String getStackTrace(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString();
    }

}
